/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is Forte for Java, Community Edition. The Initial
* Developer of the Original Code is Sun Microsystems, Inc. Portions
* Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved.
*/
package org.openide.util;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.jar.Attributes;
import java.text.MessageFormat;
/** Convenience class permitting easy loading of localized resources of various sorts.
* Extends the functionality of the default Java resource support, and interacts
* better with class loaders in a multiple-loader system.
* <p>Example usage:
* <p><code><pre>
* package com.mycom;
* public class Foo {
* // Search for tag Foo_theMessage in /com/mycom/Bundle.properties:
* private static String theMessage = {@link NbBundle#getBundle(Class) NbBundle.getBundle} (Foo.class).{@link ResourceBundle#getString(String) getString} ("Foo_theMessage");
* // Might also look in /com/mycom/Bundle_de.properties, etc.
* }
* </pre></code>
*
* @author Petr Hamernik, Jaroslav Tulach
* @version 0.33, Apr 30, 1998
*/
public class NbBundle extends Object {
/** encoding that should be used to load localized files for corona
*/
private static final String RESOURCE_ENCODING = "Cp1250"; // NOI18N
private static final boolean DEBUG = Boolean.getBoolean ("org.openide.util.NbBundle.DEBUG"); // NOI18N
/** Cache of URLs for localized files
* @associates URL*/
private static Hashtable cacheListFiles = new Hashtable();
/** Cache of ResourceBundles
* @associates RefBundle*/
private static Hashtable cacheList = new Hashtable();
/** Default class loader */
private static SystemClassLoader defaultLoader = new SystemClassLoader();
/* This interface has method for resolving the class loader. When someone
* calls resourceBundle to get any resource or class, we would like to use caller's
* class loader, because not all classes have to use the same.
* So if the ClassLoaderFinder is set, we use method find to resolve
* the appropriate classloader. Method find should look at stack and takes
* class at 5-th position (stack[4]) and return its classloader (it can be also null)
*/
/** Utility to find the class loader of the class calling a method (for use only by implementation).
* Used to find the resource bundle appropriate to a class in a different loader.
* @see NbBundle#setClassLoaderFinder
*/
public static interface ClassLoaderFinder {
/** Get the class loader of the calling method.
* @return class loader of caller
*/
public ClassLoader find();
}
/** Class Loader finder */
private static ClassLoaderFinder loaderFinder;
/* This method is used for set the classloader finder. It can be called just once
* and then NbBundle class uses the given loaderfinder for finding the
* adequate ClassLoader.
*/
/** Set the class loader finder (for use only by implementation).
* @param loaderFinder the new finder
* @throws Error if already set
*/
public static void setClassLoaderFinder(ClassLoaderFinder loaderFinder) {
if (NbBundle.loaderFinder == null)
NbBundle.loaderFinder = loaderFinder;
else
throw new Error(NbBundle.getBundle(NbBundle.class).getString("MSG_ClassFinderAlreadySet"));
}
/** Interface for all recognizer */
private static interface Recognizer {
/** Tests if recognize the resource with the specific name in the loader.
* @param loader classloader
* @param name The name of resource
* @return true if resource can be loaded using specific classloader.
*/
public boolean recognize(ClassLoader loader, String name);
}
/** Class recognizer is used for recognizing localized files with
* the specific extension.
*/
private static class ExtensionRecognizer implements Recognizer {
/** extension for recognizing objects. */
public String ext;
/** recognized object */
public URL recognURL;
/** Creates new ExtensionRecognizer for the specific extension */
public ExtensionRecognizer(String ext) {
this.ext = ext;
}
/** Tests if recognize the resource with the specific name in the loader.
* @param loader classloader
* @param name The name of resource
* @return true if resource can be loaded using specific classloader.
*/
public boolean recognize(ClassLoader loader, String name) {
recognURL = loader.getResource(name.replace('.', '/') + '.' + ext);
return (recognURL != null);
}
}
/** Class recognizer is used for recognizing classes.
*/
private static class ClassRecognizer implements Recognizer {
/** recognized object */
public Object recognObject = null;
public ClassRecognizer() {
}
/** Tests if recognize the resource with the specific name in the loader.
* @param loader classloader
* @param name The name of resource
* @return true if resource can be loaded using specific classloader.
*/
public boolean recognize(ClassLoader loader, String name) {
try {
recognObject = loader.loadClass(name).newInstance();
return true;
}
catch(Exception e) {
return false;
}
}
}
/** Get a localized file in the default locale with the default class loader.
* @param baseName base name of file, as dot-separated path (e.g. <code>some.dir.File</code>)
* @param ext extension of file
* @return URL of matching localized file
* @throws MissingResourceException if not found
*/
public static synchronized URL getLocalizedFile(String baseName, String ext)
throws MissingResourceException {
return getLocalizedFile(baseName, ext, Locale.getDefault(), getLoader());
}
/** Get a localized file with the default class loader.
* @param baseName base name of file, as dot-separated path (e.g. <code>some.dir.File</code>)
* @param ext extension of file
* @param locale locale of file
* @return URL of matching localized file
* @throws MissingResourceException if not found
*/
public static synchronized URL getLocalizedFile(String baseName, String ext,
Locale locale) throws MissingResourceException {
return getLocalizedFile(baseName, ext, locale, getLoader());
}
/** Get a localized file.
* @param baseName base name of file, as dot-separated path (e.g. <code>some.dir.File</code>)
* @param ext extension of file
* @param locale locale of file
* @param loader class loader to use
* @return URL of matching localized file
* @throws MissingResourceException if not found
*/
public static synchronized URL getLocalizedFile(String baseName, String ext,
Locale locale, ClassLoader loader) throws MissingResourceException {
URL lookup = null;
Enumeration en = getLocaleEnum(locale);
String cachePrefix = "["+Integer.toString(loader.hashCode())+"]"; // NOI18N
Vector cacheCandidates = new Vector();
ExtensionRecognizer recognizer = new ExtensionRecognizer(ext);
while (en.hasMoreElements()) {
String searchName = baseName + en.nextElement().toString();
String cacheName = cachePrefix + searchName.replace('.', '/') + '.' + ext;
lookup = (URL) cacheListFiles.get(cacheName);
if (lookup != null)
break;
cacheCandidates.addElement(cacheName);
if (recognizer.recognize(loader, searchName)) {
lookup = recognizer.recognURL;
break;
}
}
if (lookup == null) {
throw new MissingResourceException(MessageFormat.format (NbBundle.getBundle(NbBundle.class).getString("MSG_FMT_CantFindResourceFor"), new Object [] {baseName}), baseName,"");
}
else {
Enumeration enAdd = cacheCandidates.elements();
while (enAdd.hasMoreElements()) {
cacheListFiles.put(enAdd.nextElement(), lookup);
}
return lookup;
}
}
/** Find a localized value for a given key and locale.
* Scans through a map to find
* the most localized match possible. For example:
* <p><code><PRE>
* findLocalizedValue (hashTable, "keyName", new Locale ("cs_CZ"))
* </PRE></code>
* <p>This would return the first non-<code>null</code> value obtained from the following tests:
* <UL>
* <LI> <CODE>hashTable.get ("keyName_cs_CZ")</CODE>
* <LI> <CODE>hashTable.get ("keyName_cs")</CODE>
* <LI> <CODE>hashTable.get ("keyName")</CODE>
* </UL>
*
* @param table mapping from localized strings to objects
* @param key the key to look for
* @param locale the locale to use
* @return the localized object or <code>null</code> if no key matches
*/
public static Object getLocalizedValue (Map table, String key, Locale locale) {
if (table instanceof Attributes) {
throw new IllegalArgumentException ("Please do not use a java.util.jar.Attributes for NbBundle.getLocalizedValue " + // NOI18N
"without using the special form that works properly with Attributes.Name's as keys."); // NOI18N
}
Enumeration en = getLocaleEnum (locale);
while (en.hasMoreElements ()) {
Object v = table.get (key + (String)en.nextElement ());
if (v != null) {
// ok
return v;
}
}
return null;
}
/** Find a localized value for a given key in the default system locale.
*
* @param table mapping from localized strings to objects
* @param key the key to look for
* @return the localized object or <code>null</code> if no key matches
* @see #getLocalizedValue(Map,String,Locale)
*/
public static Object getLocalizedValue (Map table, String key) {
return getLocalizedValue (table, key, Locale.getDefault ());
}
/** Find a localized value in a JAR manifest.
* @param attr the manifest attributes
* @param key the key to look for (case-insensitive)
* @param locale the locale to use
* @return the value if found, else <code>null</code>
*/
public static String getLocalizedValue (Attributes attr, Attributes.Name key, Locale locale) {
return (String)getLocalizedValue (attr2Map (attr), key.toString ().toLowerCase (), locale);
}
/** Find a localized value in a JAR manifest in the default system locale.
* @param attr the manifest attributes
* @param key the key to look for (case-insensitive)
* @return the value if found, else <code>null</code>
*/
public static String getLocalizedValue (Attributes attr, Attributes.Name key) {
return (String)getLocalizedValue (attr2Map (attr), key.toString ().toLowerCase ());
}
/** Necessary because Attributes implements Map; however this is dangerous!
* The keys are Attributes.Name's, not Strings.
* Also manifest lookups should not be case-sensitive.
* (Though the locale suffix still will be!)
*/
private static Map attr2Map (final Attributes attr) {
class AttributesMap extends HashMap {
public AttributesMap () {
super (7);
}
public Object get (Object obj) {
Attributes.Name an = new Attributes.Name ((String)obj);
return attr.getValue (an);
}
}
return new AttributesMap ();
/* JST: Does the previous code do the same? I hope so...
Map result = new HashMap ();
Iterator it = attr.entrySet ().iterator ();
while (it.hasNext ()) {
Map.Entry entry = (Map.Entry) it.next ();
String newKey = ((Attributes.Name) entry.getKey ()).toString ().toLowerCase ();
String value = (String) entry.getValue ();
result.put (newKey, value);
}
return result;
*/
}
/**
* Get a resource bundle with the default class loader and locale.
* @param baseName bundle basename
* @return the resource bundle
* @exception MissingResourceException if the bundle does not exist
*/
public static final ResourceBundle getBundle(String baseName) throws MissingResourceException {
return getBundle(baseName, Locale.getDefault(), getLoader());
}
/** Get a resource bundle in the same package as the provided class,
* with the default locale and class loader.
* This is the usual style of invocation.
*
* @param clazz the class to take the package name from
* @return the resource bundle
* @exception MissingResourceException if the bundle does not exist
*/
public static ResourceBundle getBundle (Class clazz) {
String name = findName (clazz);
return getBundle(name, Locale.getDefault(), getLoader());
}
/** Finds package name for given class */
private static String findName (Class clazz) {
String pref = clazz.getName ();
int last = pref.lastIndexOf ('.');
if (last >= 0) {
pref = pref.substring (0, last + 1);
return pref + "Bundle"; // NOI18N
} else {
// base package, search for bundle
return "Bundle"; // NOI18N
}
}
/**
* Get a resource bundle with the default class loader.
* @param baseName bundle basename
* @param locale the locale to use
* @return the resource bundle
* @exception MissingResourceException if the bundle does not exist
*/
public static final ResourceBundle getBundle(String baseName, Locale locale)
throws MissingResourceException {
return getBundle(baseName, locale, getLoader());
}
/** Get a resource bundle the hard way.
* @param baseName bundle basename
* @param locale the locale to use
* @param loader the class loader to use
* @return the resource bundle
* @exception MissingResourceException if the bundle does not exist
*/
public static final ResourceBundle getBundle(String baseName, Locale locale,
ClassLoader loader) throws MissingResourceException {
Enumeration en = getLocaleEnum(locale);
RefBundle ret = findBundle(baseName, loader, en);
if (ret == null)
throw new MissingResourceException(MessageFormat.format (NbBundle.getBundle(NbBundle.class).getString("MSG_FMT_CantFindResourceFor"), new Object [] {baseName}), baseName,"");
else
return ret;
}
/** Finds Resource bundle for the baseName in the ClassLoader loader
* using enumeration of all possible localized sufixes ("_cs_CZ", "_cs", "",..)
* It returns RefBundle which is subclass of ResourceBundle class and
* chains them using 'parent' property (i.e. "_cs_CZ" -> "_cs" -> "")
*/
private static RefBundle findBundle(String baseName, ClassLoader loader, Enumeration en) {
String cachePrefix = "["+Integer.toString(loader.hashCode())+"]"; // NOI18N
Vector cacheCandidates = new Vector();
ExtensionRecognizer extRecognizer = new ExtensionRecognizer("properties"); // NOI18N
ClassRecognizer clRecognizer = new ClassRecognizer();
RefBundle lookup = null;
while (en.hasMoreElements()) {
String searchName = baseName + en.nextElement().toString();
String cacheName = cachePrefix + searchName.replace('.', '/');
lookup = (RefBundle) cacheList.get(cacheName);
if (lookup != null)
break;
cacheCandidates.addElement(cacheName);
if (clRecognizer.recognize(loader, searchName)) {
lookup = new RefBundle((ResourceBundle) clRecognizer.recognObject);
cacheList.put(cacheName, lookup);
RefBundle par = findBundle(baseName, loader, en);
lookup.setParent(par);
break;
}
if (extRecognizer.recognize(loader, searchName)) {
lookup = new RefBundle(createResourceBundleFromURL(extRecognizer.recognURL));
cacheList.put(cacheName, lookup);
RefBundle par = findBundle(baseName, loader, en);
lookup.setParent(par);
break;
}
}
if (lookup != null) {
Enumeration enAdd = cacheCandidates.elements();
while (enAdd.hasMoreElements()) {
cacheList.put((String)enAdd.nextElement(), lookup);
}
cacheCandidates.removeAllElements();
}
return lookup;
}
/** Creates input reader with present encoding, or default one,
* if the previous operation fails.
*
* @param is input stream
* @return input reader
*/
private static Reader createLoacalizedReader (InputStream is) {
try {
return new InputStreamReader (is, RESOURCE_ENCODING);
} catch (java.io.UnsupportedEncodingException ex) {
return new InputStreamReader (is);
}
}
private static int counter = 0;
/** Creates resource bundle using URL */
private static ResourceBundle createResourceBundleFromURL(URL url) {
ResourceBundle ret = null;
try {
InputStream inp = url.openStream();
counter++;
if (DEBUG) System.err.println("NbBundle counter: <" + counter + "> = " + url); // NOI18N
ret = new ReadBundle (
new BufferedReader (createLoacalizedReader (inp)),
counter
);
inp.close ();
} catch (IOException e) {
if (System.getProperty ("netbeans.debug.exceptions") != null) e.printStackTrace();
}
return ret;
}
/** @return default class loader which is used, when we don't have
* any other class loader. (in function getBundle(String), getLocalizedFile(String),
* and so on...
*/
private static ClassLoader getLoader() {
ClassLoader ret = null;
if (loaderFinder != null)
ret = loaderFinder.find();
if (ret == null)
ret = NbBundle.class.getClassLoader();
return (ret == null) ? defaultLoader : ret;
}
/** @return all possible localizing sufixes in Enumeration.
*/
private static Enumeration getLocaleEnum(Locale loc) {
return new LocaleIterator(loc);
}
/**
* The SystemClassLoader loads system classes (those in your classpath).
* This is an attempt to unify the handling of system classes and ClassLoader
* classes.
*/
private static class SystemClassLoader extends java.lang.ClassLoader {
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
return findSystemClass(name);
}
public InputStream getResourceAsStream(String name) {
return ClassLoader.getSystemResourceAsStream(name);
}
public java.net.URL getResource(String name) {
return ClassLoader.getSystemResource(name);
}
}
/** This class (enumeration) gives all localized sufixes using nextElement
* method. It goes through given Locale and continues through Locale.getDefault()
* Example 1:
* Locale.getDefault().toString() -> "_us"
* you call new LocaleIterator(new Locale("cs", "CZ"));
* ==> You will gets: "_cs_CZ", "_cs", "", "_us"
*
* Example 2:
* Locale.getDefault().toString() -> "_cs_CZ"
* you call new LocaleIterator(new Locale("cs", "CZ"));
* ==> You will gets: "_cs_CZ", "_cs", ""
*/
private static class LocaleIterator extends Object implements Enumeration {
/** this flag means, if default locale is in progress */
private boolean defaultInProgress = false;
/** this flag means, if empty sufix was exported yet */
private boolean empty = false;
/** curren locale.*/
private Locale locale;
/** current sufix which will be returned in next calling nextElement */
private String current;
/** Creates new LocaleIterator for given locale.
* @param locale given Locale
*/
public LocaleIterator(Locale locale) {
this.locale = locale;
if (locale.equals(Locale.getDefault())) {
defaultInProgress = true;
}
current = '_' + locale.toString();
}
/** @return next sufix.
* @exception NoSuchElementException if there is no more locale sufix.
*/
public Object nextElement() throws NoSuchElementException {
if (current == null)
throw new NoSuchElementException();
String ret = current;
int lastUnderbar = current.lastIndexOf('_');
if (lastUnderbar == 0) {
if (empty)
current = null;
else {
current = ""; // NOI18N
empty = true;
}
}
else {
if (lastUnderbar == -1) {
if (defaultInProgress)
current = null;
else {
locale = Locale.getDefault();
current = '_' + locale.toString();
defaultInProgress = true;
}
}
else {
current = current.substring(0, lastUnderbar);
}
}
return ret;
}
/** Tests if there is any sufix.*/
public boolean hasMoreElements() {
return (current != null);
}
}
/** Encapsulation of ResourceBundle. java.util.ResourceBundle has not protected
* variable parent and protected setter setParent, and so we have to rewrite
* this method and make it public.
*/
private static class RefBundle extends ResourceBundle {
/** Encapsulated ResourceBundle.*/
private ResourceBundle ref;
/** Creates new RefBundle for the ResourceBundle. */
public RefBundle(ResourceBundle ref) {
this.ref = ref;
}
/** Get an object from a ResourceBundle.
* @param key see class description.
*/
protected Object handleGetObject(String key) throws MissingResourceException {
Object obj;
try {
return ref.getObject(key);
}
catch (MissingResourceException e) {
if (parent != null) {
return parent.getObject(key);
}
else
throw new MissingResourceException(NbBundle.getBundle(NbBundle.class).getString("MSG_CantFindResource"), ref.getClass().getName(), key);
}
}
/**
* Return an enumeration of the keys.
*/
public Enumeration getKeys() {
return ref.getKeys();
}
/** Sets the parent of this bundle */
public void setParent(ResourceBundle parent) {
this.parent = parent;
}
/** Gets the parent of this bundle. Can returns null if this bundle has
* no parent.
*/
public ResourceBundle getParent() {
return parent;
}
}
/** ResourceBundle that reads its content from the Reader.
*/
private static class ReadBundle extends ResourceBundle {
/** the properties */
private Properties prop = new Properties ();
private int counter;
/** Reads content of the bundle from reader r.
* @param r the reader
* @param counter the bundle counter for debugging
* @exception IOExcepiton if an error occures during reading
*/
public ReadBundle (Reader r, int counter) throws IOException {
this.counter = counter;
BufferedReader in = DEBUG ? new LineNumberReader (r) : new BufferedReader (r);
int ch = in.read();
for (;;) {
switch (ch) {
case -1: return;
case '#':
case '!':
do {
ch = in.read();
} while ((ch >= 0) && (ch != '\n') && (ch != '\r'));
continue;
case '\n':
case '\r':
case ' ':
case '\t':
ch = in.read();
continue;
}
// Read the key
int line = DEBUG ? ((LineNumberReader) in).getLineNumber () + 1 : 0;
StringBuffer key = new StringBuffer();
while (
(ch >= 0) && (ch != '=') && (ch != ':') &&
(ch != ' ') && (ch != '\t') && (ch != '\n') && (ch != '\r')
) {
char _ch = (char) ch;
key.append (_ch == '\\' ? (char) in.read () : _ch);
ch = in.read();
}
while ((ch == ' ') || (ch == '\t')) {
ch = in.read();
}
if ((ch == '=') || (ch == ':')) {
ch = in.read();
}
while ((ch == ' ') || (ch == '\t')) {
ch = in.read();
}
// Read the value
StringBuffer val = new StringBuffer();
while ((ch >= 0) && (ch != '\n') && (ch != '\r')) {
int next = 0;
if (ch == '\\') {
switch (ch = in.read()) {
case '\r':
if (
((ch = in.read()) == '\n') ||
(ch == ' ') ||
(ch == '\t')
) {
// fall thru to '\n' case
} else {
continue;
}
case '\n':
while (((ch = in.read()) == ' ') || (ch == '\t'));
continue;
case 't': ch = '\t'; next = in.read(); break;
case 'n': ch = '\n'; next = in.read(); break;
case 'r': ch = '\r'; next = in.read(); break;
case 'u': {
while ((ch = in.read()) == 'u');
int d = 0;
loop:
for (int i = 0 ; i < 4 ; i++) {
next = in.read();
switch (ch) {
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
d = (d << 4) + ch - '0';
break;
case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
d = (d << 4) + 10 + ch - 'a';
break;
case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
d = (d << 4) + 10 + ch - 'A';
break;
default:
break loop;
}
ch = next;
}
ch = d;
break;
}
default: next = in.read(); break;
}
} else {
next = in.read();
}
val.append((char)ch);
ch = next;
}
// [PENDING] remove ICON_ test after search module fixed
if (DEBUG && key.toString ().indexOf ("ICON_") == -1) { // NOI18N
val.append (" <"); // NOI18N
val.append (Integer.toString (counter));
val.append (':');
val.append (Integer.toString (line));
val.append ('>');
}
prop.put(key.toString(), val.toString());
}
}
/** Gets the right object for specified key.
* @param key the key
* @return object or null for the key
*/
public Object handleGetObject (String key) {
return prop.getProperty (key);
}
/** Enumeration of keys.
* @return enumeration of Strings
*/
public Enumeration getKeys () {
Enumeration result;
if (parent != null) {
Hashtable temp = new Hashtable();
for (
Enumeration parentKeys = parent.getKeys() ;
parentKeys.hasMoreElements() ;
/* nothing */
) {
temp.put(parentKeys.nextElement(), this);
}
for (
Enumeration thisKeys = prop.keys();
thisKeys.hasMoreElements() ;
/* nothing */
) {
temp.put(thisKeys.nextElement(), this);
}
result = temp.keys();
} else {
result = prop.keys();
}
return result;
}
}
}
/*
* Log
* 17 Gandalf 1.16 1/16/00 Jesse Glick May now have escaped
* chars in bundle key (acc. to standard Properties syntax).
* 16 Gandalf 1.15 1/14/00 Jesse Glick IDE versioning fix.
* 15 Gandalf 1.14 1/12/00 Pavel Buzek I18N
* 14 Gandalf 1.13 1/12/00 Jesse Glick Better bundle debugger.
* 13 Gandalf 1.12 1/12/00 Jesse Glick -Dorg.openide.util.NbBundle.DEBUG=true
* to get bundle debugging.
* 12 Gandalf 1.11 12/9/99 Jaroslav Tulach Hopefully faster impl of
* getLocalizedValue (Attribute.Name)
* 11 Gandalf 1.10 10/22/99 Ian Formanek NO SEMANTIC CHANGE - Sun
* Microsystems Copyright in File Comment
* 10 Gandalf 1.9 7/25/99 Ian Formanek Exceptions printed to
* console only on "netbeans.debug.exceptions" flag
* 9 Gandalf 1.8 6/8/99 Ian Formanek ---- Package Change To
* org.openide ----
* 8 Gandalf 1.7 5/15/99 Jesse Glick [JavaDoc], and
* idiotproofed getLocalizedValue (Map, ...).
* 7 Gandalf 1.6 5/14/99 Jaroslav Tulach Bugfixes.
* 6 Gandalf 1.5 5/7/99 Jesse Glick Module localization.
* 5 Gandalf 1.4 4/27/99 Jesse Glick [JavaDoc] and
* generalizing Hashtable -> Map.
* 4 Gandalf 1.3 3/26/99 Ian Formanek Removed obsoleted method
* getBundle (Object)
* 3 Gandalf 1.2 3/25/99 Ales Novak
* 2 Gandalf 1.1 1/5/99 Ian Formanek Property update.
* 1 Gandalf 1.0 1/5/99 Ian Formanek
* $
* Beta Change History:
* 0 Tuborg 0.30 --/--/98 Jaroslav Tulach Reader for PropertyResourceBundle
* 0 Tuborg 0.31 --/--/98 Petr Hamernik Bug fix
* 0 Tuborg 0.32 --/--/98 Petr Hamernik -3 [Petr] class loader finder
*/